// 版权所有2009 Go作者。版权所有。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

// 包tar实现对tar档案的访问。
// 
// 磁带存档（tar）是一种文件格式，用于存储一系列文件，
// 可以以流式方式读写。
// 此软件包旨在涵盖格式的大多数变体，包括GNU和BSD tar工具生成的格式。
package tar

import (
	"errors"
	"fmt"
	"io/fs"
	"math"
	"path"
	"reflect"
	"strconv"
	"strings"
	"time"
)

// 错误：在头中使用Uid和Gid字段可能会在32位
// 体系结构上溢出。如果解码时遇到较大的值，则存储在Header中的结果
// 将是截断版本。

var (
	ErrHeader          = errors.New("archive/tar: invalid tar header")
	ErrWriteTooLong    = errors.New("archive/tar: write too long")
	ErrFieldTooLong    = errors.New("archive/tar: header field too long")
	ErrWriteAfterClose = errors.New("archive/tar: write after close")
	errMissData        = errors.New("archive/tar: sparse file references non-existent data")
	errUnrefData       = errors.New("archive/tar: sparse file contains unreferenced data")
	errWriteHole       = errors.New("archive/tar: write non-NUL byte in sparse hole")
)

type headerError []string

func (he headerError) Error() string {
	const prefix = "archive/tar: cannot encode header"
	var ss []string
	for _, s := range he {
		if s != "" {
			ss = append(ss, s)
		}
	}
	if len(ss) == 0 {
		return prefix
	}
	return fmt.Sprintf("%s: %v", prefix, strings.Join(ss, "; and "))
}

// 键入标头的标志。Typeflag。
const (
	// 类型“0”表示常规文件。
	TypeReg  = '0'
	TypeRegA = '\x00' // 不推荐使用：改用TypeReg。

	// 类型“1”到“6”仅为标题标志，可能没有数据体。
	TypeLink    = '1' // 硬链接
	TypeSymlink = '2' // 符号链接
	TypeChar    = '3' // 字符设备节点
	TypeBlock   = '4' // 块设备节点
	TypeDir     = '5' // 目录
	TypeFifo    = '6' // FIFO节点

	// 保留“7”类型。
	TypeCont = '7'

	// 类型“x”由PAX格式用于存储只与下一个文件相关的键值记录。
	// 此包透明地处理这些类型。
	TypeXHeader = 'x'

	// PAX格式使用'g'类型来存储与所有后续文件相关的键值记录。
	// 此包仅支持解析和组合此类头文件，
	// 但当前不支持跨文件持久化全局状态。
	TypeXGlobalHeader = 'g'

	// 类型“S”表示GNU格式的稀疏文件。
	TypeGNUSparse = 'S'

	// GNU格式将“L”和“K”类型用于元文件
	// 用于存储下一个文件的路径或链接名。
	// 此包透明地处理这些类型。
	TypeGNULongName = 'L'
	TypeGNULongLink = 'K'
)

// PAX扩展标题记录的关键字。
const (
	paxNone     = "" // 表示没有适合的PAX密钥
	paxPath     = "path"
	paxLinkpath = "linkpath"
	paxSize     = "size"
	paxUid      = "uid"
	paxGid      = "gid"
	paxUname    = "uname"
	paxGname    = "gname"
	paxMtime    = "mtime"
	paxAtime    = "atime"
	paxCtime    = "ctime"   // 从以后的PAX规范版本中删除，但有效
	paxCharset  = "charset" // 当前未使用的
	paxComment  = "comment" // 当前未使用的

	paxSchilyXattr = "SCHILY.xattr."

	// PAX扩展头中GNU稀疏文件的关键字。
	paxGNUSparse          = "GNU.sparse."
	paxGNUSparseNumBlocks = "GNU.sparse.numblocks"
	paxGNUSparseOffset    = "GNU.sparse.offset"
	paxGNUSparseNumBytes  = "GNU.sparse.numbytes"
	paxGNUSparseMap       = "GNU.sparse.map"
	paxGNUSparseName      = "GNU.sparse.name"
	paxGNUSparseMajor     = "GNU.sparse.major"
	paxGNUSparseMinor     = "GNU.sparse.minor"
	paxGNUSparseSize      = "GNU.sparse.size"
	paxGNUSparseRealSize  = "GNU.sparse.realsize"
)

// basicKeys是一套我们内置支持的PAX密钥。
// 这不包含“charset”或“comment”，它们都是特定于PAX的，因此不太可能将它们添加为Header的一流功能。
// 用户可以使用PAXRecords字段自行设置。
var basicKeys = map[string]bool{
	paxPath: true, paxLinkpath: true, paxSize: true, paxUid: true, paxGid: true,
	paxUname: true, paxGname: true, paxMtime: true, paxAtime: true, paxCtime: true,
}

// 头表示tar存档中的单个头。
// 某些字段可能未填充。
// 
// 为了向前兼容，用户可以从读卡器中检索标题。接下来，
// 以某种方式对其进行变异，然后将其传递回Writer。WriteHeader 
// 应该通过创建一个新的头并复制他们感兴趣保留的字段
// 来实现。
type Header struct {
	// 根据名称中是否存在尾随斜杠，零值将自动升级为TypeReg或TypeDir 
	// 类型。
	Typeflag byte

	Name     string // 文件条目名称
	Linkname string // 链接目标名称（对TypeLink或TypeSymlink有效）

	Size  int64  // 逻辑文件大小字节
	Mode  int64  // 权限和模式位
	Uid   int    // 所有者用户ID 
	Gid   int    // 所有者组ID 
	Uname string // 所有者用户名
	Gname string // 所有者组名

	// 如果未指定格式，则选择Writer。WriteHeader将ModTime 
	// 四舍五入到最接近的秒，并忽略AccessTime和ChangeTime字段。
	// 
	// 要使用AccessTime或ChangeTime，请将格式指定为PAX或GNU。
	// 要使用亚秒分辨率，请将格式指定为PAX。
	ModTime    time.Time // 修改时间
	AccessTime time.Time // 访问时间（需要PAX或GNU支持）
	ChangeTime time.Time // 更改时间（需要PAX或GNU支持）

	Devmajor int64 // 主要设备号（对TypeChar或TypeBlock有效）
	Devminor int64 // 次要设备号（对TypeChar或TypeBlock有效）

	// /“SCHILY.xattr”下的记录名称空间。
	// 
	// 以下是语义等价的：
	// h.Xattrs[key]=value 
	// h.PAXRecords[“SCHILY.xattr.”+key]=值
	// 
	// 当作家。如果调用WriteHeader，Xattrs的内容将优先于PAXRecords中的内容。
	// 
	// 已弃用：改用PAXRecords。
	Xattrs map[string]string

	// PAXRecords是PAX扩展头记录的映射。
	// 
	// 用户定义的记录应具有以下形式的键：
	// 供应商。关键字
	// 其中VENDOR是大写的名称空间，关键字
	// 不包含“=”字符（例如，“GOLANG.pkg.version”）。
	// 键和值应为非空UTF-8字符串。
	// 
	// 当作家。调用WriteHeader时，从
	// 头中的其他字段派生的PAX记录优先于PAXRecords。
	PAXRecords map[string]string

	// Format指定tar头的格式。
	// 
	// 这是由读者设置的。下一步是尽最大努力猜测格式。
	// 由于读卡器大量读取一些不兼容的文件，
	// 这可能是未知的。
	// 
	// 如果写入时未指定格式。WriteHeader被调用为
	// 然后它使用第一种格式（按USTAR、PAX、GNU的顺序）
	// 能够对这个头进行编码（参见格式）。
	Format Format
}

// sparseEntry表示文件中偏移量处长度大小的片段。
type sparseEntry struct{ Offset, Length int64 }

func (s sparseEntry) endOffset() int64 { return s.Offset + s.Length }

// 稀疏文件可以表示为SparseStatas或sparseHoles。
// 只要知道总大小，它们是等价的，一个可以转换成另一个形式并返回。支持稀疏
// 文件的各种tar格式以SPARRAS形式表示稀疏文件。也就是说，它们
// 指定文件中包含数据的片段，并将其他所有内容视为
// 具有零字节。因此，这个包
// 中的编码和解码逻辑处理SPARAS。
// 
// 但是，外部API使用sparseHoles而不是SparseTas，因为
// sparseHoles的零值在逻辑上表示一个普通文件（即，其中有
// 没有漏洞）。另一方面，sparas的零值意味着
// 文件中没有数据，这很奇怪。例如，如果基础原始文件包含10字节的数据：
// var compactFile=“abcdefgh”
// 
// ，稀疏映射有以下条目：
// var spd sparstatas=[]sparseEntry{
// 2的数据片段。。6 
// 18的数据片段。。20 
// /}
// /var sph sparseHoles=[]sparseEntry{
// 0的孔碎片。。1 
// 7的孔碎片。。17 
// 21的孔碎片。。24 
// }
// 
// 然后是结果稀疏文件的内容，并带有一个头。25的大小是：
// var sparseFile=“\x00”*2+“abcde”+“\x00”*11+“fgh”+“\x00”*4 
type (
	sparseDatas []sparseEntry
	sparseHoles []sparseEntry
)

// validateSparseEntries报告sp是否是有效的稀疏映射。
// sp代表数据片段还是孔片段并不重要。
func validateSparseEntries(sp []sparseEntry, size int64) bool {
	// 验证所有稀疏项。这些检查与
	// BSD tar实用程序执行的检查相同。
	if size < 0 {
		return false
	}
	var pre sparseEntry
	for _, cur := range sp {
		switch {
		case cur.Offset < 0 || cur.Length < 0:
			return false // 负值永远不会好
		case cur.Offset > math.MaxInt64-cur.Length:
			return false // 长度较大的整数溢出
		case cur.endOffset() > size:
			return false // 区域超出实际大小
		case pre.endOffset() > cur.Offset:
			return false // 区域不能重叠，并且必须按顺序
		}
		pre = cur
	}
	return true
}

// alignSparseEntries变异src并返回dst，其中每个片段的
// 起始偏移量与最近的块对齐边缘，每个
// 结束偏移向下对齐到最近的块边缘。
// 
// 即使Go tar阅读器和BSD tar实用程序可以处理具有任意偏移量和长度的条目，GNU tar实用程序也只能处理块大小的倍数的偏移量和长度。
func alignSparseEntries(src []sparseEntry, size int64) []sparseEntry {
	dst := src[:0]
	for _, s := range src {
		pos, end := s.Offset, s.endOffset()
		pos += blockPadding(+pos) // 向上取整至最接近的块大小
		if end != size {
			end -= blockPadding(-end) // 向下取整至最接近的块大小
		}
		if pos < end {
			dst = append(dst, sparseEntry{Offset: pos, Length: end - pos})
		}
	}
	return dst
}

// 反转SparseSentries将稀疏贴图从一种形式转换为另一种形式。
// 如果输入是sparseHoles，那么它将输出sparseHoles，反之亦然。
// 输入必须已经过验证。
// 
// 此函数变异src并返回一个规范化映射，其中：
// /*相邻片段合并在一起
// /*只有最后一个片段可能是空的
// /*最后一个片段的内偏移量是总大小
func invertSparseEntries(src []sparseEntry, size int64) []sparseEntry {
	dst := src[:0]
	var pre sparseEntry
	for _, cur := range src {
		if cur.Length == 0 {
			continue // 跳过空片段
		}
		pre.Length = cur.Offset - pre.Offset
		if pre.Length > 0 {
			dst = append(dst, pre) // 只添加非空片段
		}
		pre.Offset = cur.endOffset()
	}
	pre.Length = size - pre.Offset // 可能唯一的空片段
	return append(dst, pre)
}

// 文件状态跟踪逻辑（包括稀疏孔）和当前文件剩余的物理
// /（实际在tar存档中）字节。
// 
// 不变量：逻辑剩余>=物理剩余
type fileState interface {
	logicalRemaining() int64
	physicalRemaining() int64
}

// allowedFormats确定可以使用哪些格式。
// 返回的值是多种可能格式的逻辑OR。
// 如果值的格式未知，则无法对输入头进行编码
// 并返回一个错误，解释原因。
// 
// 作为检查字段的副产品，此函数返回paxHdrs，其中包含所有无法直接编码的字段。
// 值接收器确保此方法不会改变源标头。
func (h Header) allowedFormats() (format Format, paxHdrs map[string]string, err error) {
	format = FormatUSTAR | FormatPAX | FormatGNU
	paxHdrs = make(map[string]string)

	var whyNoUSTAR, whyNoPAX, whyNoGNU string
	var preferPAX bool // 更喜欢PAX而不是USTAR 
	verifyString := func(s string, size int, name, paxKey string) {
		// 路径和链接路径可选NUL终止符。
		// 从技术上讲，uname和gname需要它，
		// 但GNU和BSD tar都没有检查它。
		tooLong := len(s) > size
		allowLongGNU := paxKey == paxPath || paxKey == paxLinkpath
		if hasNUL(s) || (tooLong && !allowLongGNU) {
			whyNoGNU = fmt.Sprintf("GNU cannot encode %s=%q", name, s)
			format.mustNotBe(FormatGNU)
		}
		if !isASCII(s) || tooLong {
			canSplitUSTAR := paxKey == paxPath
			if _, _, ok := splitUSTARPath(s); !canSplitUSTAR || !ok {
				whyNoUSTAR = fmt.Sprintf("USTAR cannot encode %s=%q", name, s)
				format.mustNotBe(FormatUSTAR)
			}
			if paxKey == paxNone {
				whyNoPAX = fmt.Sprintf("PAX cannot encode %s=%q", name, s)
				format.mustNotBe(FormatPAX)
			} else {
				paxHdrs[paxKey] = s
			}
		}
		if v, ok := h.PAXRecords[paxKey]; ok && v == s {
			paxHdrs[paxKey] = v
		}
	}
	verifyNumeric := func(n int64, size int, name, paxKey string) {
		if !fitsInBase256(size, n) {
			whyNoGNU = fmt.Sprintf("GNU cannot encode %s=%d", name, n)
			format.mustNotBe(FormatGNU)
		}
		if !fitsInOctal(size, n) {
			whyNoUSTAR = fmt.Sprintf("USTAR cannot encode %s=%d", name, n)
			format.mustNotBe(FormatUSTAR)
			if paxKey == paxNone {
				whyNoPAX = fmt.Sprintf("PAX cannot encode %s=%d", name, n)
				format.mustNotBe(FormatPAX)
			} else {
				paxHdrs[paxKey] = strconv.FormatInt(n, 10)
			}
		}
		if v, ok := h.PAXRecords[paxKey]; ok && v == strconv.FormatInt(n, 10) {
			paxHdrs[paxKey] = v
		}
	}
	verifyTime := func(ts time.Time, size int, name, paxKey string) {
		if ts.IsZero() {
			return // 始终正常
		}
		if !fitsInBase256(size, ts.Unix()) {
			whyNoGNU = fmt.Sprintf("GNU cannot encode %s=%v", name, ts)
			format.mustNotBe(FormatGNU)
		}
		isMtime := paxKey == paxMtime
		fitsOctal := fitsInOctal(size, ts.Unix())
		if (isMtime && !fitsOctal) || !isMtime {
			whyNoUSTAR = fmt.Sprintf("USTAR cannot encode %s=%v", name, ts)
			format.mustNotBe(FormatUSTAR)
		}
		needsNano := ts.Nanosecond() != 0
		if !isMtime || !fitsOctal || needsNano {
			preferPAX = true // USTAR可能会截断亚秒测量值
			if paxKey == paxNone {
				whyNoPAX = fmt.Sprintf("PAX cannot encode %s=%v", name, ts)
				format.mustNotBe(FormatPAX)
			} else {
				paxHdrs[paxKey] = formatPAXTime(ts)
			}
		}
		if v, ok := h.PAXRecords[paxKey]; ok && v == formatPAXTime(ts) {
			paxHdrs[paxKey] = v
		}
	}

	// 检查基本字段。
	var blk block
	v7 := blk.toV7()
	ustar := blk.toUSTAR()
	gnu := blk.toGNU()
	verifyString(h.Name, len(v7.name()), "Name", paxPath)
	verifyString(h.Linkname, len(v7.linkName()), "Linkname", paxLinkpath)
	verifyString(h.Uname, len(ustar.userName()), "Uname", paxUname)
	verifyString(h.Gname, len(ustar.groupName()), "Gname", paxGname)
	verifyNumeric(h.Mode, len(v7.mode()), "Mode", paxNone)
	verifyNumeric(int64(h.Uid), len(v7.uid()), "Uid", paxUid)
	verifyNumeric(int64(h.Gid), len(v7.gid()), "Gid", paxGid)
	verifyNumeric(h.Size, len(v7.size()), "Size", paxSize)
	verifyNumeric(h.Devmajor, len(ustar.devMajor()), "Devmajor", paxNone)
	verifyNumeric(h.Devminor, len(ustar.devMinor()), "Devminor", paxNone)
	verifyTime(h.ModTime, len(v7.modTime()), "ModTime", paxMtime)
	verifyTime(h.AccessTime, len(gnu.accessTime()), "AccessTime", paxAtime)
	verifyTime(h.ChangeTime, len(gnu.changeTime()), "ChangeTime", paxCtime)

	// 检查仅标题类型。
	var whyOnlyPAX, whyOnlyGNU string
	switch h.Typeflag {
	case TypeReg, TypeChar, TypeBlock, TypeFifo, TypeGNUSparse:
		// 排除TypeLink和TypeSymlink，因为它们可能引用目录。
		if strings.HasSuffix(h.Name, "/") {
			return FormatUnknown, nil, headerError{"filename may not have trailing slash"}
		}
	case TypeXHeader, TypeGNULongName, TypeGNULongLink:
		return FormatUnknown, nil, headerError{"cannot manually encode TypeXHeader, TypeGNULongName, or TypeGNULongLink headers"}
	case TypeXGlobalHeader:
		h2 := Header{Name: h.Name, Typeflag: h.Typeflag, Xattrs: h.Xattrs, PAXRecords: h.PAXRecords, Format: h.Format}
		if !reflect.DeepEqual(h, h2) {
			return FormatUnknown, nil, headerError{"only PAXRecords should be set for TypeXGlobalHeader"}
		}
		whyOnlyPAX = "only PAX supports TypeXGlobalHeader"
		format.mayOnlyBe(FormatPAX)
	}
	if !isHeaderOnlyType(h.Typeflag) && h.Size < 0 {
		return FormatUnknown, nil, headerError{"negative size on header-only type"}
	}

	// 检查乘客记录。
	if len(h.Xattrs) > 0 {
		for k, v := range h.Xattrs {
			paxHdrs[paxSchilyXattr+k] = v
		}
		whyOnlyPAX = "only PAX supports Xattrs"
		format.mayOnlyBe(FormatPAX)
	}
	if len(h.PAXRecords) > 0 {
		for k, v := range h.PAXRecords {
			switch _, exists := paxHdrs[k]; {
			case exists:
				continue // 不要覆盖现有记录
			case h.Typeflag == TypeXGlobalHeader:
				paxHdrs[k] = v // 复制所有记录
			case !basicKeys[k] && !strings.HasPrefix(k, paxGNUSparse):
				paxHdrs[k] = v // 忽略可能冲突的本地记录
			}
		}
		whyOnlyPAX = "only PAX supports PAXRecords"
		format.mayOnlyBe(FormatPAX)
	}
	for k, v := range paxHdrs {
		if !validPAXRecord(k, v) {
			return FormatUnknown, nil, headerError{fmt.Sprintf("invalid PAX record: %q", k+" = "+v)}
		}
	}

	// TODO（dsnet）：在添加稀疏支持时重新启用此功能。
	// 请参阅https:
	/*
		// 检查稀疏文件。
		if len(h.SparseHoles) > 0 || h.Typeflag == TypeGNUSparse {
			if isHeaderOnlyType(h.Typeflag) {
				return FormatUnknown, nil, headerError{"header-only type cannot be sparse"}
			}
			if !validateSparseEntries(h.SparseHoles, h.Size) {
				return FormatUnknown, nil, headerError{"invalid sparse holes"}
			}
			if h.Typeflag == TypeGNUSparse {
				whyOnlyGNU = "only GNU supports TypeGNUSparse"
				format.mayOnlyBe(FormatGNU)
			} else {
				whyNoGNU = "GNU supports sparse files only with TypeGNUSparse"
				format.mustNotBe(FormatGNU)
			}
			whyNoUSTAR = "USTAR does not support sparse files"
			format.mustNotBe(FormatUSTAR)
		}
	*/

	// 检查所需格式。
	if wantFormat := h.Format; wantFormat != FormatUnknown {
		if wantFormat.has(FormatPAX) && !preferPAX {
			wantFormat.mayBe(FormatUSTAR) // PAX表示允许使用USTAR太
		}
		format.mayOnlyBe(wantFormat) // 设置允许的格式和需要的格式的并集
	}
	if format == FormatUnknown {
		switch h.Format {
		case FormatUSTAR:
			err = headerError{"Format specifies USTAR", whyNoUSTAR, whyOnlyPAX, whyOnlyGNU}
		case FormatPAX:
			err = headerError{"Format specifies PAX", whyNoPAX, whyOnlyGNU}
		case FormatGNU:
			err = headerError{"Format specifies GNU", whyNoGNU, whyOnlyPAX}
		default:
			err = headerError{whyNoUSTAR, whyNoPAX, whyNoGNU, whyOnlyPAX, whyOnlyGNU}
		}
	}
	return format, paxHdrs, err
}

// FileInfo返回一个fs。标题的文件信息。
func (h *Header) FileInfo() fs.FileInfo {
	return headerFileInfo{h}
}

// headerFileInfo实现fs。文件信息。
type headerFileInfo struct {
	h *Header
}

func (fi headerFileInfo) Size() int64        { return fi.h.Size }
func (fi headerFileInfo) IsDir() bool        { return fi.Mode().IsDir() }
func (fi headerFileInfo) ModTime() time.Time { return fi.h.ModTime }
func (fi headerFileInfo) Sys() any           { return fi.h }

// Name返回文件的基本名称。
func (fi headerFileInfo) Name() string {
	if fi.IsDir() {
		return path.Base(path.Clean(fi.h.Name))
	}
	return path.Base(fi.h.Name)
}

// Mode返回headerFileInfo的权限和模式位。
func (fi headerFileInfo) Mode() (mode fs.FileMode) {
	// 设置文件权限位。
	mode = fs.FileMode(fi.h.Mode).Perm()

	// 设置setuid、setgid和粘性位。
	if fi.h.Mode&c_ISUID != 0 {
		mode |= fs.ModeSetuid
	}
	if fi.h.Mode&c_ISGID != 0 {
		mode |= fs.ModeSetgid
	}
	if fi.h.Mode&c_ISVTX != 0 {
		mode |= fs.ModeSticky
	}

	// 设置文件模式位；清除烫发、setuid、setgid和粘性位。
	switch m := fs.FileMode(fi.h.Mode) &^ 07777; m {
	case c_ISDIR:
		mode |= fs.ModeDir
	case c_ISFIFO:
		mode |= fs.ModeNamedPipe
	case c_ISLNK:
		mode |= fs.ModeSymlink
	case c_ISBLK:
		mode |= fs.ModeDevice
	case c_ISCHR:
		mode |= fs.ModeDevice
		mode |= fs.ModeCharDevice
	case c_ISSOCK:
		mode |= fs.ModeSocket
	}

	switch fi.h.Typeflag {
	case TypeSymlink:
		mode |= fs.ModeSymlink
	case TypeChar:
		mode |= fs.ModeDevice
		mode |= fs.ModeCharDevice
	case TypeBlock:
		mode |= fs.ModeDevice
	case TypeDir:
		mode |= fs.ModeDir
	case TypeFifo:
		mode |= fs.ModeNamedPipe
	}

	return mode
}

// sysStat，如果非nil，则从fi的系统相关字段填充h。
var sysStat func(fi fs.FileInfo, h *Header) error

const (
	// USTAR规范中的模式常量：
	// 请参阅http:
	c_ISUID = 04000 // 设置uid 
	c_ISGID = 02000 // 设置gid 
	c_ISVTX = 01000 // 保存文本（粘性位）

	// 通用Unix模式常量；这些在任何通用的焦油标准中都没有定义。
	// 标题。FileInfo理解这些，但FileInfoHeader永远不会产生这些。
	c_ISDIR  = 040000  // 目录
	c_ISFIFO = 010000  // FIFO 
	c_ISREG  = 0100000 // 常规文件
	c_ISLNK  = 0120000 // 符号链接
	c_ISBLK  = 060000  // 块特殊文件
	c_ISCHR  = 020000  // 字符特殊文件
	c_ISSOCK = 0140000 // 套接字
)

// FileInfoHeader从fi创建部分填充的头。
// 如果fi描述了符号链接，则FileInfoHeader会将链接记录为链接目标。
// 如果fi描述一个目录，名称后面会加一个斜杠。
// 
// 。FileInfo的Name方法只返回
// 它描述的文件的基本名称，可能需要修改头。Name 
// 提供文件的完整路径名。
func FileInfoHeader(fi fs.FileInfo, link string) (*Header, error) {
	if fi == nil {
		return nil, errors.New("archive/tar: FileInfo is nil")
	}
	fm := fi.Mode()
	h := &Header{
		Name:    fi.Name(),
		ModTime: fi.ModTime(),
		Mode:    int64(fm.Perm()), // 或后面使用c_IS*常量
	}
	switch {
	case fm.IsRegular():
		h.Typeflag = TypeReg
		h.Size = fi.Size()
	case fi.IsDir():
		h.Typeflag = TypeDir
		h.Name += "/"
	case fm&fs.ModeSymlink != 0:
		h.Typeflag = TypeSymlink
		h.Linkname = link
	case fm&fs.ModeDevice != 0:
		if fm&fs.ModeCharDevice != 0 {
			h.Typeflag = TypeChar
		} else {
			h.Typeflag = TypeBlock
		}
	case fm&fs.ModeNamedPipe != 0:
		h.Typeflag = TypeFifo
	case fm&fs.ModeSocket != 0:
		return nil, fmt.Errorf("archive/tar: sockets not supported")
	default:
		return nil, fmt.Errorf("archive/tar: unknown file mode %v", fm)
	}
	if fm&fs.ModeSetuid != 0 {
		h.Mode |= c_ISUID
	}
	if fm&fs.ModeSetgid != 0 {
		h.Mode |= c_ISGID
	}
	if fm&fs.ModeSticky != 0 {
		h.Mode |= c_ISVTX
	}
	// 如果可能，从操作系统特定的
	// 文件信息字段填充其他字段。
	if sys, ok := fi.Sys().(*Header); ok {
		// 此文件信息来自头文件（而不是操作系统）。使用
		// 原始标题填充所有剩余字段。
		h.Uid = sys.Uid
		h.Gid = sys.Gid
		h.Uname = sys.Uname
		h.Gname = sys.Gname
		h.AccessTime = sys.AccessTime
		h.ChangeTime = sys.ChangeTime
		if sys.Xattrs != nil {
			h.Xattrs = make(map[string]string)
			for k, v := range sys.Xattrs {
				h.Xattrs[k] = v
			}
		}
		if sys.Typeflag == TypeLink {
			// 硬链接
			h.Typeflag = TypeLink
			h.Size = 0
			h.Linkname = sys.Linkname
		}
		if sys.PAXRecords != nil {
			h.PAXRecords = make(map[string]string)
			for k, v := range sys.PAXRecords {
				h.PAXRecords[k] = v
			}
		}
	}
	if sysStat != nil {
		return h, sysStat(fi, h)
	}
	return h, nil
}

// isHeaderOnlyType检查给定的类型标志是否属于没有
// 数据节的类型，即使指定了大小也是如此。
func isHeaderOnlyType(flag byte) bool {
	switch flag {
	case TypeLink, TypeSymlink, TypeChar, TypeBlock, TypeDir, TypeFifo:
		return true
	default:
		return false
	}
}

func min(a, b int64) int64 {
	if a < b {
		return a
	}
	return b
}
